﻿using System;
using System.Configuration;
using System.Globalization;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Security;
using HttpAuth.Configuration;

namespace HttpAuth
{
    public class HttpAuthenticationModule : IHttpModule
    {
        public HttpAuthSection HttpAuthSection { get {
            return ConfigurationManager.GetSection("httpAuth") as HttpAuthSection;
        } }

        protected static NonceCouner NonceCouner = new HttpAuth.NonceCouner();

        public void Init(HttpApplication context)
        {
            context.AuthenticateRequest += new EventHandler(OnEnter);
            context.EndRequest += new EventHandler(OnLeave);
        }

        void OnEnter(object sender, EventArgs e)
        {
            var app = sender as HttpApplication;
            if (HttpAuthSection == null || HttpAuthSection.Mode == HttpAuthMode.None) return;
            if (app.Request.Headers.AllKeys.Contains("Authorization") == false) return;
            
            var authHdVal = app.Request.Headers["Authorization"];
            if (HttpAuthSection.Mode == HttpAuthMode.Basic && authHdVal.StartsWith("Basic"))
            {
                var decodedBytes = Convert.FromBase64String(authHdVal.Substring(6));
                var decodedString = Encoding.ASCII.GetString(decodedBytes);
                var parts = decodedString.Split(':');
                var username = parts[0];
                var password = parts[1];
                var roles = new string[0];

                if (HttpAuthSection.Credentials.Source == HttpAuthCredentialSource.Inline)
                {
                    var credential = HttpAuthSection.Credentials.Users[username];
                    if (credential == null) return;
                    var credentialPwd = credential.Password;

                    var pwdFormat = HttpAuthSection.Credentials.PasswordFormat;
                    if (pwdFormat != "Clear")
                    {
                        password = FormsAuthentication.HashPasswordForStoringInConfigFile(password, pwdFormat).ToLower();
                        credentialPwd = credentialPwd.ToLower();
                    }
                    if (credentialPwd == password)
                    {
                        app.Context.User = new GenericPrincipal(new GenericIdentity(username), roles: new string[0]);
                    }
                    return;
                }

                Func<string, string, bool> validateUser =
                    HttpAuthSection.Credentials.Source == HttpAuthCredentialSource.FormsAuthentication ? 
                        (Func<string, string, bool>)FormsAuthentication.Authenticate :
                    HttpAuthSection.Credentials.Source == HttpAuthCredentialSource.MembershipProvider ? 
                        (Func<string, string, bool>)Membership.ValidateUser :
                    null;
                if (validateUser(username, password))
                {
                    app.Context.User = new GenericPrincipal(new GenericIdentity(username), roles: new string[0]);
                }
            }

            else if (HttpAuthSection.Mode == HttpAuthMode.Digest && authHdVal.StartsWith("Digest"))
            {
                var vals = Regex.Matches(authHdVal, @"(?<name>\w+)=(""(?<val>[^""]*)""|(?<val>[^"" ,\t\r\n]+))").Cast<Match>()
                    .ToDictionary(m => m.Groups["name"].Value, m => m.Groups["val"].Value);

                NonceCouner.Sweep(HttpAuthSection.Timeout);
                var nonce = vals["nonce"];
                var nonceCounter = default(NonceCounterItem);
                if (NonceCouner.TryGetValue(nonce, out nonceCounter) == false) return;
                var nc = vals["nc"];
                lock (nonceCounter)
                {
                    var intnc = int.Parse(nc, NumberStyles.AllowHexSpecifier);
                    if (nonceCounter.RegistPassedCount(intnc) == false) return;
                }

                var username = vals["username"];
                if (HttpAuthSection.Credentials.PasswordFormat != "MD5Digest") throw new InvalidOperationException();
                var credential = HttpAuthSection.Credentials.Users[username];
                if (credential == null) return;

                var uri = vals["uri"];
                var realm = vals["realm"];
                var cnonce = vals["cnonce"];
                var qop = vals["qop"];
                var a1 = credential.Password;
                var a2 = MD5(app.Request.HttpMethod + ":" + uri);

                var recalcRes = MD5(string.Format("{0}:{1}:{2}:{3}:{4}:{5}", a1, nonce, nc, cnonce, qop, a2));
                if (vals["response"] != recalcRes) return;
                
                app.Context.User = new GenericPrincipal(new GenericIdentity(username), roles: new string[0]);
            }
        }

        private string MD5(string p)
        {
            return FormsAuthentication.HashPasswordForStoringInConfigFile(p, "MD5").ToLower();
        }

        void OnLeave(object sender, EventArgs e)
        {
            if (HttpAuthSection == null) return;

            var app = sender as HttpApplication;
            if (app.Context.Response.StatusCode == 401 && HttpAuthSection.Mode != HttpAuthMode.None)
            {
                var realm = HttpAuthSection.Realm;
                var authHdVal = default(string);
                switch (HttpAuthSection.Mode)
                {
                    case HttpAuthMode.Basic:
                        authHdVal = "Basic Realm=" + realm;
                        break;
                    case HttpAuthMode.Digest:
                        authHdVal = string.Format(@"Digest realm=""{0}"", nonce=""{1}"", algorithm=MD5, qop=""auth""", realm, NonceCouner.NewNonce());
                        break;
                    default:
                        break;
                }
                app.Response.AppendHeader("WWW-Authenticate", authHdVal);
            }
        }

        public void Dispose()
        {
        }
    }
}
